{
 "cells": [
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "# Открытый курс по машинному обучению. Сессия № 3"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Автор материала: Минасян Гульнара (Slack: @Gulnara)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "# Предсказание рекомендует клиент интернет-магазина товар или нет по тексту отзыва\n",
    "## 1. Описание набора данных и признаков¶"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "В исследовании используется датасет, содержащий данные о пользователях интернет-магазина одежды, их отзывы на купленные товары, а также бинарный признак, указывающий. рекомендует клиент данный товар к покупке или нет.  \n",
    "**Цель исследования**: определить, будет ли человек рекомендовать покупку другим на основе оставленного отзыва и других признаков. Будем решать задачу бинарной классификации"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Данные здесь https://www.kaggle.com/nicapotato/womens-ecommerce-clothing-reviews"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Датасет включает 23486 строки и 10 столбцов переменных. Каждая строка соответствует отзыву покупателя и включает следующие переменные: \n",
    "\n",
    "**Clothing ID**: Целочисленная категориальная переменная, обозначающая конкретный товар, на который дается отзыв.  \n",
    "**Age**: Положительная целочисленная переменная, обозначающая возраст покупателя, оставившего отзыв.  \n",
    "**Title**: Строковая переменная, содержащая заголовок отзыва.   \n",
    "**Review Text**: Строковая переменная, содержащая тело отзыва.    \n",
    "**Rating**: Положительная целочисленная категориальная переменная отражает оценку товара покупателем, от 1 до 5, где 1 - худшая оценка, а 5 - наилучшая.  \n",
    "**Recommended IND**: Бинарная переменная, указывающая, рекомендует клиент продукт или нет, где рекомендует - 1, 0 - не рекомендует.    \n",
    "**Positive Feedback Count**: Положительная целочисленная переменная, отражающая количество других клиентов, считающих отзыв положительным.  \n",
    "**Division Name**: Наименование категории товара первого порядка.   \n",
    "**Department Name**: Наименование категории товара второго порядка.   \n",
    "**Class Name**: Наименование категории товара третьего порядка.  "
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## 2. Первичный анализ данных"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "import numpy as np\n",
    "import pandas as pd\n",
    "import seaborn as sns\n",
    "from sklearn.feature_extraction.text import CountVectorizer\n",
    "from sklearn.linear_model import LogisticRegression\n",
    "from sklearn.svm import LinearSVC\n",
    "from sklearn.metrics import roc_auc_score, f1_score, recall_score, precision_score\n",
    "\n",
    "%matplotlib inline\n",
    "from matplotlib import pyplot as plt\n",
    "plt.style.use(['seaborn-darkgrid'])\n",
    "plt.rcParams['figure.figsize'] = (12, 9)\n",
    "plt.rcParams['font.family'] = 'DejaVu Sans'"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "df = pd.read_csv('Womens Clothing E-Commerce Reviews.csv')"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "df = df.drop('Unnamed: 0', axis=1)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "df.info()"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "df.head()"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## 3. Первичный визуальный анализ данных"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Посмотрим какие категории включают в себя Division Name, Department Name, Class Name и как они распределены"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "#Division Name\n",
    "plt.figure(figsize=(10,4))\n",
    "ax = sns.countplot(df['Division Name'])\n",
    "\n",
    "ax.set_xticklabels(ax.get_xticklabels(), rotation=90, ha=\"right\")\n",
    "plt.tight_layout()\n",
    "plt.show()\n",
    "\n",
    "#Department Name\n",
    "plt.figure(figsize=(10,4))\n",
    "ax = sns.countplot(df['Department Name'])\n",
    "\n",
    "ax.set_xticklabels(ax.get_xticklabels(), rotation=90, ha=\"right\")\n",
    "plt.tight_layout()\n",
    "plt.show()\n",
    "\n",
    "#Department Name\n",
    "plt.figure(figsize=(10,4))\n",
    "ax = sns.countplot(df['Class Name'])\n",
    "\n",
    "ax.set_xticklabels(ax.get_xticklabels(), rotation=90, ha=\"right\")\n",
    "plt.tight_layout()\n",
    "plt.show()"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Оценим распределение клиентов интернет-магазина по возрасту"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "plt.figure(figsize=(10,4))\n",
    "ax = sns.countplot(df['Age'])\n",
    "\n",
    "ax.set_xticklabels(ax.get_xticklabels(), rotation=90, ha=\"right\")\n",
    "plt.tight_layout()\n",
    "plt.show()"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Разобьем клиентов на возрастные группы для удобства дальнейшего анализа:  \n",
    "**18 - 30** - 1 группа  \n",
    "**30 - 40** - 1 группа  \n",
    "**40 - 50** - 3 группа  \n",
    "**50 - 60** - 4 группа  \n",
    "**60 - 70** - 5 группа  \n",
    "**свыше 70** - 6 группа  "
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "df['Age_str'] = df['Age']"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "df.loc[df['Age_str'] <= 30, 'Age_str'] = 1\n",
    "df.loc[(df['Age_str'] >= 31) & (df['Age_str'] <= 40), 'Age_str'] = 2\n",
    "df.loc[(df['Age_str'] >= 41) & (df['Age_str'] <= 50), 'Age_str'] = 3\n",
    "df.loc[(df['Age_str'] >= 51) & (df['Age_str'] <= 60), 'Age_str'] = 4\n",
    "df.loc[(df['Age_str'] >= 61) & (df['Age_str'] <= 70), 'Age_str'] = 5\n",
    "df.loc[(df['Age_str'] > 70), 'Age_str'] = 6"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "df.head()"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Заменим пропуски в данных. Поскольку, как мы увидели в информации о данных пропуски наблюдаются в категориальных данных, заменим их на категорию nan"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "df['Title'] = df['Title'].fillna('nan')\n",
    "df['Review Text'] = df['Review Text'].fillna('nan')\n",
    "df['Division Name'] = df['Division Name'].fillna('nan')\n",
    "df['Department Name'] = df['Department Name'].fillna('nan')\n",
    "df['Class Name'] = df['Class Name'].fillna('nan')"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "df.head()"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "plt.figure(figsize=(10,4))\n",
    "ax = sns.countplot(df['Age_str'])\n",
    "\n",
    "ax.set_xticklabels(ax.get_xticklabels(), rotation=90, ha=\"right\")\n",
    "plt.tight_layout()\n",
    "plt.show()"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Оценим сколько клиентов в выборке рекомендуют и нерекомендуют товар после покупки"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "df_recommended = df[df['Recommended IND'] == 1]\n",
    "df_not_recommended = df[df['Recommended IND'] == 0]"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "df_recommended.shape"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "df_not_recommended.shape"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Посмотрим распределение в данных выборках по возрастам"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "plt.figure(figsize=(10,4))\n",
    "ax = sns.countplot(df_recommended['Age_str'])\n",
    "\n",
    "\n",
    "ax.set_xticklabels(ax.get_xticklabels(), rotation=90, ha=\"right\")\n",
    "plt.tight_layout()\n",
    "plt.show()\n",
    "\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "ax1 = sns.countplot(df_not_recommended['Age_str'])\n",
    "ax1.set_xticklabels(ax.get_xticklabels(), rotation=90, ha=\"right\")\n",
    "plt.tight_layout()\n",
    "plt.show()"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## 4. Инсайты, найденные зависимости "
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Проведем анализ влияния признаков на целевую переменную:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "def plot_distribution(df, var, target, yl=4, **kwargs):\n",
    "    row = kwargs.get('row', None)\n",
    "    col = kwargs.get('col', None)\n",
    "    facet = sns.FacetGrid(df, hue=target, aspect=4, row=row, col=col)\n",
    "    facet.map(sns.kdeplot, var, shade=True)\n",
    "    facet.set(xlim=(0, df[var].max()), ylim=(0, yl))\n",
    "    facet.add_legend()\n",
    "    plt.show()"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "plot_distribution(df, 'Age_str', 'Recommended IND', 1.2)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Исходя из этого графика, мы видим, что люди возрастом от 30 до 50 лет чаще нерекомендуют товар (чаще недовольны товаром)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "plot_distribution(df, 'Rating', 'Recommended IND', 3)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "С рейтингом связь очевидна, чем выше бал рейтинга, тем больше человек рекомнедуют товар, однако вызывает интерес рейтинг 3, когда оценка нейтральна, однако количество нерекомендаций выше и 4, когда рейтинг хороший, однако есть некоторое количество людей, не рекомендующих товар."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## 5. Выбор метрики"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Они выбранные модели сравнивались на основе трёх метрик: точность, полнота и F-мера. Доля правильных решений классификатора (accuracy) не использовалась для сопоставления моделей, так как при обучении на несбалансированных данных она не даёт никакой важной информации."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## 6. Выбор модели"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Для обучения модели были выбраны два наиболее часто используемых алгоритма машинного обучения для работы с несбалансированными данными: логистическая регрессия и метод опорных векторов с линейным ядром (Linear SVM). "
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## 7. Предобработка данных"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Разделим выборку на обучающую и тестовую в соотношении 70 к 30"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "from sklearn.model_selection import train_test_split\n",
    "\n",
    "train, test = train_test_split(df, test_size=0.3)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Зададим вектор признаков и целевую переменную"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "X_train, y_train = train.drop('Recommended IND', axis = 1), train['Recommended IND']\n",
    "X_test, y_test = test.drop('Recommended IND', axis = 1), test['Recommended IND']"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Проведем подготовку и токенизацию отзывов и их тем при помощи \"мешка слов\""
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "X_text_train = X_train['Review Text']\n",
    "X_text_test = X_test['Review Text']"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "cv = CountVectorizer()\n",
    "cv.fit(X_text_train)\n",
    "\n",
    "len(cv.vocabulary_)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Закодируем предложения из текстов обучающей выборки индексами входящих слов. Используем разреженный формат."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "X_text_train_cv = cv.transform(X_text_train)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Преобразуем так же тестовую выборку."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "X_text_test_cv = cv.transform(X_text_test)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Обучим логистическую регрессию на данных отзывов"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "%%time\n",
    "logit = LogisticRegression(n_jobs=-1, random_state=7)\n",
    "logit.fit(X_text_train_cv, y_train)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Обучим LinearSVC"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "%%time\n",
    "svm = LinearSVC(random_state=7)\n",
    "svm.fit(X_text_train_cv, y_train)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## 8. Кросс-валидация и настройка гиперпараметров модели"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "%%time\n",
    "from sklearn.pipeline import make_pipeline\n",
    "\n",
    "text_pipe_logit = make_pipeline(CountVectorizer(), \n",
    "                                LogisticRegression(n_jobs=-1, random_state=7))\n",
    "\n",
    "text_pipe_logit.fit(X_text_train, y_train)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "%%time\n",
    "from sklearn.model_selection import GridSearchCV\n",
    "\n",
    "param_grid_logit = {'logisticregression__C': \n",
    "                    np.logspace(-5, 0, 6)}\n",
    "grid_logit = GridSearchCV(text_pipe_logit, param_grid_logit, cv=3, n_jobs=-1)\n",
    "\n",
    "grid_logit.fit(X_text_train, y_train)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Лучшее значение C и соответствующее качество на кросс-валидации."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "grid_logit.best_params_, grid_logit.best_score_"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "То же самое для LinearSVC."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "%%time\n",
    "text_pipe_svm = make_pipeline(CountVectorizer(), LinearSVC(random_state=7))\n",
    "\n",
    "text_pipe_svm.fit(X_text_train, y_train)\n",
    "print(text_pipe_svm.score(X_text_test, y_test))"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "%%time\n",
    "param_grid_svm = {'linearsvc__C': np.logspace(-5, 0, 6)}\n",
    "grid_svm = GridSearchCV(text_pipe_svm, param_grid_svm, cv=3, n_jobs=-1)\n",
    "\n",
    "grid_svm.fit(X_text_train, y_train);"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "grid_svm.best_params_, grid_svm.best_score_"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## 10. Построение кривых валидации и обучения"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "def plot_grid_scores(grid, param_name):\n",
    "    plt.plot(grid.param_grid[param_name], grid.cv_results_['mean_train_score'],\n",
    "        color='green', label='train')\n",
    "    plt.plot(grid.param_grid[param_name], grid.cv_results_['mean_test_score'],\n",
    "        color='red', label='test')\n",
    "    plt.legend();"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "plot_grid_scores(grid_logit, 'logisticregression__C')"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "grid_svm.best_params_, grid_svm.best_score_"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "plot_grid_scores(grid_svm, 'linearsvc__C')"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## 11. Прогноз для тестовой или отложенной выборки"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "%%time\n",
    "logit = LogisticRegression(C = 0.1, n_jobs=-1, random_state=7)\n",
    "logit.fit(X_text_train_cv, y_train)\n",
    "logit_pred = logit.predict(X_text_test_cv)\n",
    "\n",
    "a = round(recall_score(y_test, logit_pred), 5)\n",
    "b = round(precision_score(y_test, logit_pred), 5)\n",
    "c = round(f1_score(y_test, logit_pred), 5)\n",
    "\n",
    "print('Recall score :', a, 'Precision score:', b, 'F1 score:', c)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "%%time\n",
    "svm = LinearSVC(C = 0.01, random_state=7)\n",
    "svm.fit(X_text_train_cv, y_train)\n",
    "svm_pred = svm.predict(X_text_test_cv)\n",
    "\n",
    "a = round(recall_score(y_test, svm_pred), 5)\n",
    "b = round(precision_score(y_test, svm_pred), 5)\n",
    "c = round(f1_score(y_test, svm_pred), 5)\n",
    "\n",
    "print('Recall score :', a, 'Precision score:', b, 'F1 score:', c)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Выводы"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "По итогу были построены модели логистической регрессии и опорных векторов, предсказывающие по тексту отзыва, рекомендует ли клиент этот товар. Было выявлено, что модель опорных векторов справляется с этой задачей лучше по параметрам точность, F-мера. По параметру полнота получился примерно одинаковый результат.\n",
    "\n",
    "Полученное качество модели логистической регрессии, оцененное с помощью метрики F-меры составляет 0.94, что говорит о высокой предсказательной способности модели.\n",
    "\n",
    "Использовать данную модель можно при анализе отзывов на товары в социальных сетях, либо интернет магазинах.\n",
    "\n",
    "Возможные улучшения:\n",
    "\n",
    "добавить в модель дополнительные признаки,  \n",
    "провести более детмальную токенизацию с удалением стоп слов,  \n",
    "проанализровать наиболее часто встречаемые слова в отзывах, за которыми следует рекомендация или нет.\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": []
  }
 ],
 "metadata": {
  "kernelspec": {
   "display_name": "Python 3",
   "language": "python",
   "name": "python3"
  },
  "language_info": {
   "codemirror_mode": {
    "name": "ipython",
    "version": 3
   },
   "file_extension": ".py",
   "mimetype": "text/x-python",
   "name": "python",
   "nbconvert_exporter": "python",
   "pygments_lexer": "ipython3",
   "version": "3.6.4"
  }
 },
 "nbformat": 4,
 "nbformat_minor": 2
}
